今天要來介紹一個 DB
功能 seed data
,有的時候當我們開發 application
時,很常會需要一些測試資料,那假設今天我們的測試資料很多,總不可能一個一個手動去新增,而且有時候也需要定期清楚過期的資料,為了簡化這些事提高開發效率,所以這時候 seed data
就派上用場了,以下我們將慢慢介紹~
seed data
大致上有以下的功能:
application
需要的測試資料migrate DB
時候 reset
你的測試資料所以 seed data
就是讓你開發時候有資料可以測試外,同時 migrate
時候也會自動重新塞新的資料,解決 schema
不一致的問題
在 prisma
中如果要使用 seed data
功能,需要在 package.json
中加上 seed
的 key
在 prisma
這個欄位,然後當你要執行 seed data
的時候你只要run
prisma db seed
,如此就會知道你 seed data
的檔案在哪裡
// package.json
"prisma": {
"seed": "ts-node prisma/seed.ts"
},
如果使用 ts-node
的話要注意一件事情,就是 ts-node
他會幫你做 transpiling
跟 typechecking
,那其實 typechecking
是可以被 disabled
,只需要在你的 cli
加上 --transpile-only
,這樣 ts-node
就不會執行 typechecking
,這個參數非常好用因為他可以減少 memory (RAM)
的使用,同時加快 seed data
的執行效率
"seed": "ts-node --transpile-only prisma/seed.ts"
Datebase seeding
在 prisma
中有兩種方式 :
prisma db seed
手動載入prisma migrate reset
自動執行 seed data
或是 prisma migrate dev
(某些情況)使用 prisma db seed
手動方式,他的好處在於你可以測試 seed data
是否正確同時也會是你在開發前的前置作業需要確認的事情
另外當你 prisma db seed
都沒問題的時候,就可以透過 prisma migrate reset
自動執行,他會有以下的步驟:
prisma migrate reset
這個 cli
database
會執行 prisma migrate dev
去跟去你 migration
的記錄解決 schema
的衝突prisma db seed
另外假設你不需要 seed data
在你執行,在 prisma migrate reset
或是 prisma migrate dev
只要加上 skip-seed
這個 flag
就好
以下是今天的 model
model User {
id String @id @default(cuid())
name String
age Int?
profileViews Int
country String
city String
email String @unique
}
之後我們在 prisma
這個資料夾新增 seed.ts
的檔案,這邊簡單解釋一下 seed.ts
做了什麼事情:
@faker-js
產生 mock data
prisma.user.deleteMany
每次清空 seed data
prisma.user.upsert
當 email
有存在就跳過 craete
,確保 seed data
過程中 email
可能會有重複,原因是我們的 email
的 schema
是 unique
的//prisma/seed.ts
import { faker } from "@faker-js/faker";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient()
const main = async () => {
await prisma.user.deleteMany({})
for (let i = 0; i < 20; i++) {
const userEmail = faker.internet.email()
await prisma.user.upsert({
where: {
email: userEmail
},
update: {},
create: {
email: userEmail,
name: faker.person.fullName(),
age: faker.helpers.maybe(() => faker.number.int({ min: 0, max: 100 }), { probability: 0.8 }),
profileViews: faker.number.int({ min: 0, max: 5000 }),
country: faker.location.country(),
city: faker.location.city()
}
})
}
}
main()
.then(async () => {
await prisma.$disconnect()
})
.catch(async (e) => {
console.error(e)
await prisma.$disconnect()
process.exit(1)
})
那因為在 age
我們的 schema
是 optional
的,所以為了讓 seed
的更真實,@faker-js
有提供 maybe
的用法,有機率的 return data
用這個 utils
來模擬 user
沒有 age
的資料
所以以下的 code
會是有八成的機率 age
會有資料,否則會是 null
//..
age: faker.helpers.maybe(() => faker.number.int({ min: 0, max: 100 }), { probability: 0.8 })
//..
之後到 package.json
加上 prisma script
//package.json
{
"name": "my-project",
"version": "1.0.0",
"prisma": {
"seed": "ts-node prisma/seed.ts"
},
"devDependencies": {
"@types/node": "^14.14.21",
"ts-node": "^9.1.1",
"typescript": "^4.1.3"
}
}
執行 seed script
>npx prisma db seed
接著我們到 prisma studio
檢查一下確實 age
有些資料是 null
的
另外 ts-node
有提供不同 compiler
的選項,如果你是使用 Nextjs
的話需要根據以下的寫法去轉換一下 module
"prisma": {
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
},
其實在 prisma
中你還可以透過 Raw SQL
幫你 seed data
,使用 prisma.$executeRaw
就可以幫你完成
async function rawSql() {
const result = await prisma.$executeRaw`INSERT INTO "User" ("id", "email", "name") VALUES (3, 'foo@example.com', 'Foo') ON CONFLICT DO NOTHING;`
console.log({ result })
}
這邊你可以透過 Raw SQL
幫你新增額外的 seed data
main()
.then(rawSql)
.then(async () => {
await prisma.$disconnect()
})
.catch(async (e) => {
console.error(e)
await prisma.$disconnect()
process.exit(1)
})
另外還有一個今天的主角 @snaplet/seed
他也是另外一套 seed data
的 toolkit
他的好處是可以自動幫你提供 seed data
的 type
外,同時也幫你優化 seed data
的效率,然後 @snaplet/seed
目前支援 PostgreSQL
、SQLite
and MySQL
使用前要先 init @snaplet/seed
>npx @snaplet/seed init prisma/seed
他會幫你在 prisma
資料夾中產生以下的檔案:
seed.config.ts
: seed
相關的 config
seed.ts
: 執行 seed
的位置我們可以看到 seed.config.ts
主要就是透過 adapter
去 connect
PrismaClient
//seed.config.ts
import { SeedPrisma } from "@snaplet/seed/adapter-prisma";
import { defineConfig } from "@snaplet/seed/config";
import { PrismaClient } from "@prisma/client";
export default defineConfig({
adapter: () => {
const client = new PrismaClient();
return new SeedPrisma(client);
},
select: ["!*_prisma_migrations"],
});
@snaplet/seed
的寫法很單純,他會自動幫你判別你的 model
需要什麼欄位,自動幫你填上,這邊簡單解釋一下 code
:
$resetDatabase
每次都清空 DB
await seed.user((createMany) => createMany(10))
創建10筆 user
的資料// import { createSeedClient, SeedClient } from "@snaplet/seed";
const main = async () => {
// Truncate all tables in the database
await seed.$resetDatabase();
await seed.user((createMany) => createMany(10));
// Type completion not working? You might want to reload your TypeScript Server to pick up the changes
console.log(`Database seeded successfully!`);
process.exit();
};
main()
.then(async () => {
await prisma.$disconnect()
})
.catch(async (e) => {
console.error(e)
await prisma.$disconnect()
process.exit(1)
})
這邊讀者打算把 ts-node
改成 tsx
,因為 tsx
他可以讓你在執行時候不用考慮 module
問題~
>npm install -D tsx
最後別忘記到 package.json
中修改一下 prisma
的 script
//package.json
{
"name": "my-project",
"version": "1.0.0",
"prisma": {
"seed": "tsx prisma/seed/seed.ts"
},
"devDependencies": {
"@types/node": "^14.14.21",
"tsx": "^4.7.2",
"typescript": "^4.1.3"
}
}
執行 seed data
>npx prisma db seed
但是筆者在查看 studio
後,發現資料內容,只是單純的塞假文字進去,這樣的 seed data
沒有還原真實資料的情況
目前筆者找到解決方式有兩種,一種是塞 env
,因為 @snaplet/seed
有支援 openai
,透過 openai
優化你 response
部分
//.env
OPENAI_API_KEY="your_token"
另外一種就是透過 @faker-js
,這邊我們寫一個 seedData
function
,然後你會發現我們是透過迴圈方式,去 createMany
一筆資料,那是因為假如是 createMany(100)
,對 @snaplet/seed
來說他實際並不是跑 100
次的 query
而是只有一次
const seedData = async ({
seed,
count
}: {
seed: SeedClient,
count: number
}) => {
for (let i = 0; i < count; i++) {
await seed.user((createMany) => createMany(1, {
name: faker.person.fullName(),
age: faker.number.int({ min: 0, max: 100 }),
profileViews: faker.number.int({ min: 0, max: 5000 }),
country: faker.location.country(),
city: faker.location.city(),
email: faker.internet.email()
}));
}
}
那這樣透過 faker
出來的資料因為執行次數的關係,資料都是一樣的,所以筆者才會改成遞迴方式,而且這樣的 email
也會有問題就不會是唯一性了
await seed.user((createMany) => createMany(100, {
name: faker.person.fullName(),
age: faker.number.int({ min: 0, max: 100 }),
profileViews: faker.number.int({ min: 0, max: 5000 }),
country: faker.location.country(),
city: faker.location.city(),
email: faker.internet.email()
}));
完整的 code
如下
const main = async () => {
const seed = await createSeedClient();
// Truncate all tables in the database
await seed.$resetDatabase();
await seedData({ seed, count: 20 })
// Type completion not working? You might want to reload your TypeScript Server to pick up the changes
console.log(`Database seeded successfully!`);
process.exit();
};
另外如果你想自動執行 seed
的話,只需要在 package.json
的 script
加上 migrate
跟 postmigrate
//package.json
{
"name": "my-project",
"version": "1.0.0",
"prisma": {
"seed": "npx tsx prisma/seed/seed.ts"
},
"scripts": {
"migrate": "prisma migrate dev",
"postmigrate": "npx @snaplet/seed sync"
},
"devDependencies": {
"@types/node": "^14.14.21",
"tsx": "^4.7.2",
"typescript": "^4.1.3"
}
}
這樣當你 npm run migrate
就會幫你自動 seed data
了
>npm run migrate
> prisma@1.0.0 migrate
> prisma migrate dev
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": SQLite database "dev.db" at "file:./dev.db"
Already in sync, no schema change or pending migration was found.
✔ Generated Prisma Client (v5.19.1) to ./node_modules/@prisma/client in 54ms
> prisma@1.0.0 postmigrate
> npx @snaplet/seed sync
最後補充一個有趣的小 tips
我們其實可以根據下不同的 flag
在執行 prisma db seed
的時候,有不同的 seed data
的結果,只需要用 nodejs
原生的parseArgs
去讀取 cli
的參數,以下的範例就是區分 environment
有 development
跟 test
,development
有20筆資料 test
則是只有10筆,那讀者可以根據不同情況調整需求~
import { faker } from "@faker-js/faker";
import { PrismaClient } from "@prisma/client";
import { createSeedClient, SeedClient } from "@snaplet/seed";
import { parseArgs, ParseArgsConfig } from "util";
const main = async () => {
const {
values: { environment },
} = parseArgs({ options })
const seed = await createSeedClient();
// Truncate all tables in the database
await seed.$resetDatabase();
switch (environment) {
case 'development':
seedData({ seed, count: 20 })
break
case 'test':
seedData({ seed, count: 10 })
break
default:
break
}
// Type completion not working? You might want to reload your TypeScript Server to pick up the changes
console.log(`environment(${environment}): Database seeded successfully!`);
process.exit();
};
最後只要下以下的 cli
就成功了~
>npx prisma db seed -- --environment development
✅ 前端社群 :
https://lihi3.cc/kBe0Y